Skip to content

S15-07 算法-堆结构

[TOC]

堆结构

概述

堆(Heap) 是一种特殊的树形数据结构,堆的特点是每个节点的值都与其子节点的值进行比较,满足特定的堆性质。通常用于实现优先队列动态排序等应用。

堆相对于前面的数据结构来说,要稍微难理解一点。

实现:堆使用完全二叉树来实现

分类:堆可以进行很多分类,但是平时使用的基本都是二叉堆;二叉堆又可以划分为最大堆最小堆

  • 最大堆(Max Heap):每个父节点的值 >= 其子节点的值。
  • 最小堆(Min Heap):每个父节点的值 <= 其子节点的值。

image-20250123115353119

最值问题

对于每一个新的数据结构,我们都需要搞清楚为什么需要它,这是我们能够记住并且把握它的关键。它到底帮助我们解决了什么问题?

案例:获取集合的最大、最小值:

如果有一个集合,我们希望获取其中的最大值或者最小值,有哪些方案呢?

  • 数组/链表:时间复杂度是O(n)
    • 优化方法:可以进行排序,但是我们只是获取最大值或者最小值而已,但排序本身就会消耗性能;
  • 哈希表:不需要考虑了;
  • 二叉搜索树:时间复杂度是O(logn)
    • 缺点:二叉搜索树操作较为复杂,并且还要维护树的平衡时才是O(logn)级别;
  • 堆结构推荐,这个时候需要一种数据结构来解决这个问题,就是堆结构。

堆的特性

用途堆结构通常是用来解决Top K问题的

  • Top K问题 是指在一组数据中,找出最前面的K个最大/最小的元素;常用的解决方案有使用排序算法快速选择算法堆结构等;

表现形式:二叉堆用树形结构表示出来是一颗完全二叉树

底层实现:底层会使用数组来实现

索引计算公式:每个节点在数组中对应的索引i有如下的规律:

  • 如果 i = 0 ,它是根节点
  • 父节点:Math.floor((i–1)/2)
  • 左子节点:2i+1
  • 右子节点:2i+2

image-20250126132126313

最大堆的实现

API

接下来,让我们对堆结构进行设计,看看需要有哪些属性和方法。

常见属性:

  • data,存储堆中的元素,通常使用数组来实现。
  • size,堆中当前元素的数量。

常见方法:

  • insert()(value),在堆中插入一个新元素。
  • extract() / delete()(),从堆中提取/删除最大或最小元素。
  • peek()(),返回堆中的最大或最小元素。
  • isEmpty()(),判断堆是否为空。
  • build_heap()(list),通过一个列表来构造堆。

那么接下来我们就来实现这个堆结构吧!

封装-Heap

封装Heap的类:

  • data,存储堆中的元素,通常使用数组来实现。
  • size,堆中当前元素的数量。

image-20250126150826504

封装-swap()

swap()(i,j),私有方法,交换i和j这2个值。

image-20250126151324434

封装-peek()

peek()(),返回堆中的最大或最小元素。

image-20250126151608209

封装-size()

size()(),堆中当前元素的数量。

image-20250126151557319

封装-isEmpty()

isEmpty()(),判断堆是否为空。

image-20250126151539756

封装-insert()

insert()(value),在堆中插入一个新元素。

最大堆插入思路:每次插入元素后,需要对堆进行重构,以维护最大堆的性质,该维护策略称为上滤(percolate up)

  • 1、将新元素直接添加到数组的最后位置

  • 2、检测是否符合最大堆的特性:

    • 符合:不再操作
    • 不符合:将新插入的元素进行上滤操作
  • 上滤操作:

    • 1、新元素索引:index = data.length - 1
    • 2、父元素索引:parentIndex = Math.floor((index - 1) /2)
    • 3、将新元素和父元素进行比较:
      • 如果新元素 <= 父元素,直接break
      • 如果新元素 > 父元素,交换二者swap,同时修改index为父元素的索引parentIndex
    • 4、进行下一次循环操作,直到index <= 0

图解:


代码实现:

image-20250126170044741

测试: [19, 100, 36, 17, 3, 25, 1, 2, 7]

image-20250126165938378


代码抽取:

image-20250126170205840

封装-delete()

extract() / delete()(),从堆中提取/删除最大或最小元素。

最大堆删除思路:每次删除元素后,需要对堆进行重构,以维护最大堆的性质,该维护策略称为下滤(percolate down)

  • 1、将最后的元素赋值给被删除元素,这样不会破坏最大堆的结构
  • 2、对最后的元素进行下滤操作
  • 下滤操作:
    • 1、当前节点(最后的节点)索引:index=0
    • 2、左子节点索引:leftChildIndex = 2 * index + 1
    • 3、右子节点索引:rightChildIndex = 2 * index + 2
    • 4、比较2个子节点值的大小,找到较大的值:largerIndex
    • 5、比较当前节点和较大的子节点的值:
      • 如果子节点值 < 当前节点,直接break
      • 如果子节点值 >= 当前节点,交换二者swap,同时修改index为largerIndex
    • 6、进行下一次循环操作,直到2*index + 1 >= this.length

图解:

删除120

删除100


代码实现:

image-20250126212558272

测试: 从大到小依次弹出

image-20250126212606777

封装-buildHeap()

build_heap()(list),通过一个列表来构造堆。

原地建堆(In-place heap construction) 是指建立堆的过程中,不使用额外的内存空间,直接在原有数组上进行操作。

图解: [9,11,20,56,23,45]


代码实现:

image-20250126215820178

测试:

image-20250126215839767

这种原地建堆的方式,我们称之为自下而上的下滤。也可以使用自上而下的上滤,但是效率较低,作为课下自行研究。


优化:在构造函数中原地建堆

image-20250126220927104

最小堆的实现

1、修改插入操作的heapify_up()方法

image-20250126221831843

测试:

image-20250126221434258

2、修改删除操作的heapify_down()方法

image-20250126221418581

测试:

image-20250126221543739

二叉堆的实现

在一个类中同时实现最大堆和最小堆,根据传入的参数isMax不同分别生成最大堆和最小堆。

1、添加isMax属性,标记最大堆、最小堆

image-20250126222951244

2、封装私有方法,比较2个索引的值

image-20250126223030751

3、修改heapify_up()方法,调用compare()方法进行比较

image-20250126223043716

4、修改heapify_down()方法,调用compare()方法进行比较

image-20250126223132391

数据结构可视化网站

在线数据结构演练: